/*
 * Copyright 2002-2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.config.annotation.web.configurers;

import javax.servlet.http.HttpServletRequest;

import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
import org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter;

/**
 * Adds X509 based pre authentication to an application. Since validating the
 * certificate happens when the client connects, the requesting and validation
 * of the client certificate should be performed by the container. Spring Security
 * will then use the certificate to look up the {@link Authentication} for the user.
 *
 * <h2>Security Filters</h2>
 *
 * The following Filters are populated
 *
 * <ul>
 * <li>{@link X509AuthenticationFilter}</li>
 * </ul>
 *
 * <h2>Shared Objects Created</h2>
 *
 * The following shared objects are created
 *
 * <ul>
 * <li>
 * {@link AuthenticationEntryPoint}
 * is populated with an {@link Http403ForbiddenEntryPoint}</li>
 * <li>A {@link PreAuthenticatedAuthenticationProvider} is populated into
 * {@link HttpSecurity#authenticationProvider(org.springframework.security.authentication.AuthenticationProvider)}
 * </li>
 * </ul>
 *
 * <h2>Shared Objects Used</h2>
 *
 * The following shared objects are used:
 *
 * <ul>
 * <li>A {@link UserDetailsService} shared object is used if no {@link AuthenticationUserDetailsService} is specified</li>
 * </ul>
 *
 * @author Rob Winch
 * @since 3.2
 */
public final class X509Configurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<X509Configurer<H>,H> {
    private X509AuthenticationFilter x509AuthenticationFilter;
    private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService;
    private String subjectPrincipalRegex;
    private AuthenticationDetailsSource<HttpServletRequest, PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails> authenticationDetailsSource;

    /**
     * Creates a new instance
     * @see HttpSecurity#x509()
     */
    public X509Configurer() {
    }

    /**
     * Allows specifying the entire {@link X509AuthenticationFilter}. If this is
     * specified, the properties on {@link X509Configurer} will not be
     * populated on the {@link X509AuthenticationFilter}.
     *
     * @param x509AuthenticationFilter the {@link X509AuthenticationFilter} to use
     * @return the {@link X509Configurer} for further customizations
     */
    public X509Configurer<H> x509AuthenticationFilter(
            X509AuthenticationFilter x509AuthenticationFilter) {
        this.x509AuthenticationFilter = x509AuthenticationFilter;
        return this;
    }

    /**
     * Specifies the {@link AuthenticationDetailsSource}
     *
     * @param authenticationDetailsSource the {@link AuthenticationDetailsSource} to use
     * @return the {@link X509Configurer} to use
     */
    public X509Configurer<H> authenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails> authenticationDetailsSource) {
        this.authenticationDetailsSource = authenticationDetailsSource;
        return this;
    }

    /**
     * Shortcut for invoking {@link #authenticationUserDetailsService(AuthenticationUserDetailsService)} with a {@link UserDetailsByNameServiceWrapper}.
     *
     * @param userDetailsService the {@link UserDetailsService} to use
     * @return the {@link X509Configurer} for further customizations
     */
    public X509Configurer<H> userDetailsService(
            UserDetailsService userDetailsService) {
        UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService = new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>();
        authenticationUserDetailsService.setUserDetailsService(userDetailsService);
        return authenticationUserDetailsService(authenticationUserDetailsService);
    }

    /**
     * Specifies the {@link AuthenticationUserDetailsService} to use. If not
     * specified, the shared {@link UserDetailsService} will be used to create a
     * {@link UserDetailsByNameServiceWrapper}.
     *
     * @param authenticationUserDetailsService the {@link AuthenticationUserDetailsService} to use
     * @return the {@link X509Configurer} for further customizations
     */
    public X509Configurer<H> authenticationUserDetailsService(
            AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService) {
        this.authenticationUserDetailsService = authenticationUserDetailsService;
        return this;
    }

    /**
     * Specifies the regex to extract the principal from the certificate. If not
     * specified, the default expression from
     * {@link SubjectDnX509PrincipalExtractor} is used.
     *
     * @param subjectPrincipalRegex
     *            the regex to extract the user principal from the certificate
     *            (i.e. "CN=(.*?)(?:,|$)").
     * @return the {@link X509Configurer} for further customizations
     */
    public X509Configurer<H> subjectPrincipalRegex(String subjectPrincipalRegex) {
        this.subjectPrincipalRegex = subjectPrincipalRegex;
        return this;
    }

    @Override
    public void init(H http) throws Exception {
        PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
        authenticationProvider.setPreAuthenticatedUserDetailsService(getAuthenticationUserDetailsService(http));

        http
            .authenticationProvider(authenticationProvider)
            .setSharedObject(AuthenticationEntryPoint.class,new Http403ForbiddenEntryPoint());
    }

    @Override
    public void configure(H http) throws Exception {
        X509AuthenticationFilter filter = getFilter(http.getSharedObject(AuthenticationManager.class));
        http.addFilter(filter);
    }

    private X509AuthenticationFilter getFilter(
            AuthenticationManager authenticationManager) {
        if (x509AuthenticationFilter == null) {
            x509AuthenticationFilter = new X509AuthenticationFilter();
            x509AuthenticationFilter.setAuthenticationManager(authenticationManager);
            if(subjectPrincipalRegex != null) {
                SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();
                principalExtractor.setSubjectDnRegex(subjectPrincipalRegex);
                x509AuthenticationFilter.setPrincipalExtractor(principalExtractor);
            }
            if(authenticationDetailsSource != null) {
                x509AuthenticationFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
            }
            x509AuthenticationFilter = postProcess(x509AuthenticationFilter);
        }

        return x509AuthenticationFilter;
    }

    private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> getAuthenticationUserDetailsService(H http) {
        if(authenticationUserDetailsService == null) {
            userDetailsService(http.getSharedObject(UserDetailsService.class));
        }
        return authenticationUserDetailsService;
    }

}